/*
 * @(#)Game.java
 *
 * This work is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This work is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * Copyright (c) 2003 Per Cederberg. All rights reserved.
 */
package tetris;

import event.GameOverEvent;
import event.GameOverEventListener;
import genetics.Grid;
import java.awt.Button;
import java.awt.Checkbox;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Label;
import java.awt.Rectangle;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Random;

/**
 * The Tetris game. This class controls all events in the game and handles all
 * the game logics. The game is started through user interaction with the
 * graphical game component provided by this class.
 *
 * @version 1.2
 * @author Per Cederberg, per@percederberg.net
 */
public class Game extends Object
{

    public GeneticAI ai = new GeneticAI();
    public static Random rnd = new Random(1);
    /**
     * The main square board. This board is used for the game itself.
     */
    private SquareBoard board = null;
    /**
     * The preview square board. This board is used to display a preview of the
     * figures.
     */
    private SquareBoard previewBoard;
    private boolean isPaused = false;
    /**
     * The figures used on both boards. All figures are reutilized in order to
     * avoid creating new objects while the game is running. Special care has to
     * be taken when the preview figure and the current figure refers to the
     * same object.
     */
    private Figure[] figures = {
        new Figure(Figure.SQUARE_FIGURE),
        new Figure(Figure.LINE_FIGURE),
        new Figure(Figure.S_FIGURE),
        new Figure(Figure.Z_FIGURE),
        new Figure(Figure.RIGHT_ANGLE_FIGURE),
        new Figure(Figure.LEFT_ANGLE_FIGURE),
        new Figure(Figure.TRIANGLE_FIGURE)
    };
    /**
     * The graphical game component. This component is created on the first call
     * to getComponent().
     */
    private GamePanel component = null;
    /**
     * The thread that runs the game. When this variable is set to null, the
     * game thread will terminate.
     */
    private GameThread thread = null;
    /**
     * The game level. The level will be increased for every 20 lines removed
     * from the square board.
     */
    private int level = 1;
    /**
     * The current score. The score is increased for every figure that is
     * possible to place on the main board.
     */
    private int score = 0;
    /**
     * The current figure. The figure will be updated when
     */
    private Figure figure = null;
    /**
     * The next figure.
     */
    private Figure nextFigure = null;
    /**
     * The rotation of the next figure.
     */
    private int nextRotation = 0;
    private boolean enableGUI;
    private boolean randomizeSeed = true;
    /**
     * The figure preview flag. If this flag is set, the figure will be shown in
     * the figure preview board.
     */
    private boolean preview = true;
    /**
     * The move lock flag. If this flag is set, the current figure cannot be
     * moved. This flag is set when a figure is moved all the way down, and
     * reset when a new figure is displayed.
     */
    private boolean moveLock = false;
    private long gameSeed = -1;

    /**
     * Creates a new Tetris game. The square board will be given the default
     * size of 10x20.
     */
    public Game(boolean enableGUI)
    {
        this(10, 20, enableGUI);
        this.enableGUI = enableGUI;
    }

    /**
     * Creates a new Tetris game. The square board will be given the specified
     * size.
     *
     * @param width the width of the square board (in positions)
     * @param height the height of the square board (in positions)
     */
    public Game(int width, int height, boolean enableGUI)
    {
        this.enableGUI = enableGUI;
        previewBoard = new SquareBoard(5, 5, enableGUI);
        board = new SquareBoard(width, height, enableGUI);
        board.setMessage("Press start");
        thread = new GameThread();
    }

    public void setSeed(long seed)
    {
        this.gameSeed = seed;
    }

    /**
     * Set this to true if you want the seed to be randomized every new game
     * (everytime handlestart is called). Can be used with setSeed to only use a
     * specific seed.
     *
     * @param randomizeSeed
     */
    public void setRandomizeSeed(boolean randomizeSeed)
    {
        this.randomizeSeed = randomizeSeed;
    }

    /**
     * Kills the game running thread and makes necessary clean-up. After calling
     * this method, no further methods in this class should be called. Neither
     * should the component returned earlier be trusted upon.
     */
    public void quit()
    {
        thread = null;
    }

    /**
     * Returns a new component that draws the game.
     *
     * @return the component that draws the game
     */
    public Component getComponent()
    {
        if (component == null) {
            component = new GamePanel();
        }
        return component;
    }
    /**
     * Handles a game start event. Both the main and preview square boards will
     * be reset, and all other game parameters will be reset. Finally the game
     * thread will be launched.
     */
    boolean fullSpeed = true;
    boolean aiPlay = true;
    boolean isPopulated = false;
    Grid grid = null;

    private void handleStart()
    {
        //if(isPopulated){
        //    Grid grid = new Grid();
        //    grid.populateGrid();      
        //}

        //grid.games();

        //long seed = Integer.parseInt(component.seed.getText());
        if (gameSeed == -1 || randomizeSeed) {
            gameSeed = System.currentTimeMillis();
        }
        rnd = new Random(gameSeed);
        aiPlay = component.ai.getState();
        fullSpeed = component.fullSpeed.getState();
        // Reset score and figures
        level = 1;
        score = 0;
        figure = null;
        nextFigure = randomFigure();
        nextFigure.rotateRandom();
        nextRotation = nextFigure.getRotation();

        // Reset components
        board.setMessage(null);
        board.clear();
        previewBoard.clear();
        handleLevelModification();
        handleScoreModification();
        component.button.setLabel("Pause");

        // Start game thread

        thread.reset();
    }

    /**
     * Handles a game over event. This will stop the game thread, reset all
     * figures and print a game over message.
     */
    private void handleGameOver()
    {

        // Stop game thread
        // thread.setPaused(true);

        // Reset figures
        if (figure != null) {
            figure.detach();
        }
        figure = null;
        if (nextFigure != null) {
            nextFigure.detach();
        }
        nextFigure = null;

        fireGameOverEvent(new GameOverEvent(score));

        //handleStart();

        thread.setPaused(true);


        // Handle components
        //board.setMessage("Game Over");
        //component.button.setLabel("Start");
    }

    /**
     * Handles a game pause event. This will pause the game thread and print a
     * pause message on the game board.
     */
    private void handlePause()
    {
        thread.setPaused(true);
        board.setMessage("Paused");
        component.button.setLabel("Resume");
    }

    /**
     * Handles a game resume event. This will resume the game thread and remove
     * any messages on the game board.
     */
    private void handleResume()
    {
        board.setMessage(null);
        component.button.setLabel("Pause");
        thread.setPaused(false);
    }

    /**
     * Handles a level modification event. This will modify the level label and
     * adjust the thread speed.
     */
    private void handleLevelModification()
    {
        component.levelLabel.setText("Level: " + level);
        thread.adjustSpeed();
    }

    /**
     * Handle a score modification event. This will modify the score label.
     */
    private void handleScoreModification()
    {
        component.scoreLabel.setText("Score: " + score);
    }

    /**
     * Handles a figure start event. This will move the next figure to the
     * current figure position, while also creating a new preview figure. If the
     * figure cannot be introduced onto the game board, a game over event will
     * be launched.
     */
    private void handleFigureStart()
    {
        int rotation;

        // Move next figure to current
        figure = nextFigure;
        moveLock = false;
        rotation = nextRotation;
        nextFigure = randomFigure();
        nextFigure.rotateRandom();
        nextRotation = nextFigure.getRotation();

        // Handle figure preview
        if (preview) {
            previewBoard.clear();
            nextFigure.attach(previewBoard, true);
            nextFigure.detach();
        }

        // Attach figure to game board
        figure.setRotation(rotation);
        if (!figure.attach(board, false)) {
            previewBoard.clear();
            figure.attach(previewBoard, true);
            figure.detach();
            handleGameOver();
        }
        if (aiPlay) {
            ai.makeBestMove(board.getBoard(), figure);
        }
    }

    /**
     * Handles a figure landed event. This will check that the figure is
     * completely visible, or a game over event will be launched. After this
     * control, any full lines will be removed. If no full lines could be
     * removed, a figure start event is launched directly.
     */
    private void handleFigureLanded()
    {

        // Check and detach figure
        if (figure.isAllVisible()) {
            score += 10;
            handleScoreModification();
        } else {
            handleGameOver();
            return;
        }
        figure.detach();
        figure = null;

        // Check for full lines or create new figure
        if (board.hasFullLines()) {
            board.removeFullLines();
            if (level < 9 && board.getRemovedLines() / 20 > level) {
                level = board.getRemovedLines() / 20;
                handleLevelModification();
            }
        } else {
            handleFigureStart();
        }
    }

    /**
     * Handles a timer event. This will normally move the figure down one step,
     * but when a figure has landed or isn't ready other events will be
     * launched. This method is synchronized to avoid race conditions with other
     * asynchronous events (keyboard and mouse).
     */
    private synchronized void handleTimer()
    {
        if (figure == null) {
            handleFigureStart();
        } else if (figure.hasLanded()) {
            handleFigureLanded();
        } else {
            figure.moveDown();
        }
    }

    /**
     * Handles a button press event. This will launch different events depending
     * on the state of the game, as the button semantics change as the game
     * changes. This method is synchronized to avoid race conditions with other
     * asynchronous events (timer and keyboard).
     */
    private synchronized void handleButtonPressed()
    {
        if (nextFigure == null) {
            handleStart();
        } else if (thread.isPaused()) {
            handleResume();
        } else {
            handlePause();
        }
    }

    /**
     * Handles a keyboard event. This will result in different actions being
     * taken, depending on the key pressed. In some cases, other events will be
     * launched. This method is synchronized to avoid race conditions with other
     * asynchronous events (timer and mouse).
     *
     * @param e the key event
     */
    private synchronized void handleKeyEvent(KeyEvent e)
    {

        // Handle start, pause and resume
        if (e.getKeyCode() == KeyEvent.VK_P) {
            handleButtonPressed();
            return;
        }

        // Don't proceed if stopped or paused
        if (figure == null || moveLock || thread.isPaused()) {
            return;
        }

        // Handle remaining key events
        switch (e.getKeyCode()) {

            case KeyEvent.VK_LEFT:
                figure.moveLeft();
                break;

            case KeyEvent.VK_RIGHT:
                figure.moveRight();
                break;

            case KeyEvent.VK_DOWN:
                figure.moveAllWayDown();
                moveLock = true;
                break;

            case KeyEvent.VK_UP:
            case KeyEvent.VK_SPACE:
                if (e.isControlDown()) {
                    figure.rotateRandom();
                } else if (e.isShiftDown()) {
                    figure.rotateClockwise();
                } else {
                    figure.rotateCounterClockwise();
                }
                break;

            case KeyEvent.VK_S:
                if (level < 9) {
                    level++;
                    handleLevelModification();
                }
                break;

            case KeyEvent.VK_N:
                preview = !preview;
                if (preview && figure != nextFigure) {
                    nextFigure.attach(previewBoard, true);
                    nextFigure.detach();
                } else {
                    previewBoard.clear();
                }
                break;
        }
    }

    /**
     * Returns a random figure. The figures come from the figures array, and
     * will not be initialized.
     *
     * @return a random figure
     */
    private Figure randomFigure()
    {
        return figures[(int) (rnd.nextDouble() * figures.length)];
    }

    /**
     * The game time thread. This thread makes sure that the timer events are
     * launched appropriately, making the current figure fall. This thread can
     * be reused across games, but should be set to paused state when no game is
     * running.
     */
    private class GameThread extends Thread
    {

        /**
         * The game pause flag. This flag is set to true while the game should
         * pause.
         */
        private boolean paused = true;
        /**
         * The number of milliseconds to sleep before each automatic move. This
         * number will be lowered as the game progresses.
         */
        private int sleepTime = 500;

        /**
         * Creates a new game thread with default values.
         */
        public GameThread()
        {
        }

        /**
         * Resets the game thread. This will adjust the speed and start the game
         * thread if not previously started.
         */
        public void reset()
        {
            adjustSpeed();
            setPaused(false);
            if (!isAlive()) {
                this.start();
            }
        }

        /**
         * Checks if the thread is paused.
         *
         * @return true if the thread is paused, or false otherwise
         */
        public boolean isPaused()
        {
            return paused;
        }

        /**
         * Sets the thread pause flag.
         *
         * @param paused the new paused flag value
         */
        public void setPaused(boolean paused)
        {
            this.paused = paused;
        }

        /**
         * Adjusts the game speed according to the current level. The sleeping
         * time is calculated with a function making larger steps initially an
         * smaller as the level increases. A level above ten (10) doesn't have
         * any further effect.
         */
        public void adjustSpeed()
        {
            sleepTime = 4500 / (level + 5) - 250;
            if (sleepTime < 50) {
                sleepTime = 50;
            }
        }

        /**
         * Runs the game.
         */
        public void run()
        {
            while (thread == this) {
                // Make the time step
                handleTimer();

                if (!fullSpeed) {
                    // Sleep for some time
                    try {
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException ignore) {
                        // Do nothing
                    }//*/
                } else {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException ignore) {
                        // Do nothing
                    }//*/
                }

                // Sleep if paused
                while (paused && thread == this) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException ignore) {
                        // Do nothing
                    }
                }
            }
        }
    }

    /**
     * A game panel component. Contains all the game components.
     */
    private class GamePanel extends Container
    {

        /**
         * The component size. If the component has been resized, that will be
         * detected when the paint method executes. If this value is set to
         * null, the component dimensions are unknown.
         */
        private Dimension size = null;
        /**
         * The score label.
         */
        private Label scoreLabel = new Label("Score: 0");
        /**
         * The level label.
         */
        private Label levelLabel = new Label("Level: 1");
        /**
         * The generic button.
         */
        private Button button = new Button("Start");
        public TextField seed = new TextField("1");
        public Checkbox ai = new Checkbox("AI play", true);
        public Checkbox fullSpeed = new Checkbox("Full speed", true);

        /**
         * Creates a new game panel. All the components will also be added to
         * the panel.
         */
        public GamePanel()
        {
            super();
            initComponents();
        }

        /**
         * Paints the game component. This method is overridden from the default
         * implementation in order to set the correct background color.
         *
         * @param g the graphics context to use
         */
        public void paint(Graphics g)
        {
            Rectangle rect = g.getClipBounds();

            if (size == null || !size.equals(getSize())) {
                size = getSize();
                resizeComponents();
            }
            g.setColor(getBackground());
            g.fillRect(rect.x, rect.y, rect.width, rect.height);
            super.paint(g);
        }

        /**
         * Initializes all the components, and places them in the panel.
         */
        private void initComponents()
        {
            GridBagConstraints c;

            // Set layout manager and background
            setLayout(new GridBagLayout());
            setBackground(Configuration.getColor("background", "#d4d0c8"));

            // Add game board
            c = new GridBagConstraints();
            c.gridx = 0;
            c.gridy = 0;
            c.gridheight = 8;
            c.weightx = 1.0;
            c.weighty = 1.0;
            c.fill = GridBagConstraints.BOTH;
            this.add(board.getComponent(), c);

            // Add next figure board
            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 0;
            c.weightx = 0.2;
            c.weighty = 0.18;
            c.fill = GridBagConstraints.BOTH;
            c.insets = new Insets(15, 15, 0, 15);
            this.add(previewBoard.getComponent(), c);

            // Add score label
            scoreLabel.setForeground(Configuration.getColor("label",
                    "#000000"));
            scoreLabel.setAlignment(Label.CENTER);
            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 1;
            c.weightx = 0.3;
            c.weighty = 0.05;
            c.anchor = GridBagConstraints.CENTER;
            c.fill = GridBagConstraints.BOTH;
            c.insets = new Insets(0, 15, 0, 15);
            this.add(scoreLabel, c);




            // Add level label
            levelLabel.setForeground(Configuration.getColor("label",
                    "#000000"));
            levelLabel.setAlignment(Label.CENTER);
            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 2;
            c.weightx = 0.3;
            c.weighty = 0.05;
            c.anchor = GridBagConstraints.CENTER;
            c.fill = GridBagConstraints.BOTH;
            c.insets = new Insets(0, 15, 0, 15);
            this.add(levelLabel, c);

            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 3;
            c.weightx = 0.3;
            c.weighty = 0.05;
            c.anchor = GridBagConstraints.CENTER;
            c.fill = GridBagConstraints.BOTH;
            c.insets = new Insets(0, 15, 0, 15);
            this.add(seed, c);

            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 4;
            c.weightx = 0.3;
            c.weighty = 0.05;
            c.anchor = GridBagConstraints.CENTER;
            c.fill = GridBagConstraints.BOTH;
            c.insets = new Insets(0, 15, 0, 15);
            this.add(ai, c);

            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 5;
            c.weightx = 0.3;
            c.weighty = 0.05;
            c.anchor = GridBagConstraints.CENTER;
            c.fill = GridBagConstraints.BOTH;
            c.insets = new Insets(0, 15, 0, 15);
            this.add(fullSpeed, c);

            // Add generic button
            button.setBackground(Configuration.getColor("button", "#d4d0c8"));
            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 6;
            c.weightx = 0.3;
            c.weighty = 1.0;
            c.anchor = GridBagConstraints.NORTH;
            c.fill = GridBagConstraints.HORIZONTAL;
            c.insets = new Insets(15, 15, 15, 15);
            this.add(button, c);

            // Add event handling            
            enableEvents(KeyEvent.KEY_EVENT_MASK);
            this.addKeyListener(new KeyAdapter()
            {

                public void keyPressed(KeyEvent e)
                {
                    handleKeyEvent(e);
                }
            });

            button.addActionListener(new ActionListener()
            {

                public void actionPerformed(ActionEvent e)
                {
                    handleButtonPressed();
                    component.requestFocus();
                }
            });
        }

        /**
         * Resizes all the static components, and invalidates the current
         * layout.
         */
        private void resizeComponents()
        {
            Dimension size = scoreLabel.getSize();
            Font font;
            int unitSize;

            // Calculate the unit size
            size = board.getComponent().getSize();
            size.width /= board.getBoardWidth();
            size.height /= board.getBoardHeight();
            if (size.width > size.height) {
                unitSize = size.height;
            } else {
                unitSize = size.width;
            }

            // Adjust font sizes
            font = new Font("SansSerif",
                    Font.BOLD,
                    3 + (int) (unitSize / 1.8));
            scoreLabel.setFont(font);
            levelLabel.setFont(font);
            font = new Font("SansSerif",
                    Font.PLAIN,
                    2 + unitSize / 2);
            button.setFont(font);

            // Invalidate layout
            scoreLabel.invalidate();
            levelLabel.invalidate();
            button.invalidate();
        }
    }

    /**
     * Tests the current disc.
     *
     * @return the fitness (score) of the current gene.
     */
    public void tester()
    {
        handleStart();
        //while(nextFigure != null){
        //}
        //return(getScore());
    }

    public int getScore()
    {
        return this.score;
    }
    protected javax.swing.event.EventListenerList listenerList =
            new javax.swing.event.EventListenerList();

    public void addMyEventListener(GameOverEventListener listener)
    {
        listenerList.add(GameOverEventListener.class, listener);
    }

    public void removeMyEventListener(GameOverEventListener listener)
    {
        listenerList.remove(GameOverEventListener.class, listener);
    }

    void fireGameOverEvent(GameOverEvent evt)
    {
        Object[] listeners = listenerList.getListenerList();
        // Each listener occupies two elements - the first is the listener class
        // and the second is the listener instance
        for (int i = 0; i < listeners.length; i += 2) {
            if (listeners[i] == GameOverEventListener.class) {
                ((GameOverEventListener) listeners[i + 1]).myEventOccurred(evt);
                break;
            }
        }
    }
}
